contents

세그먼트 트리(Segment Tree) 는 배열에 대한 구간 쿼리(range query) 를 효율적으로 처리할 수 있는 다재다능한 트리 기반의 자료구조입니다. 이 자료구조의 가장 큰 강점은 구간 쿼리와 점 업데이트(point update)를 모두 로그 시간 복잡도(O(logn))에 처리할 수 있다는 점으로, 동적 배열 문제에서 단순한 접근법보다 훨씬 뛰어난 성능을 보입니다.


문제점: 구간 쿼리

숫자 배열이 있고, 이 배열에 대해 두 가지 연산을 자주 수행해야 한다고 상상해 보세요.

  1. 쿼리: 주어진 구간 [l, r](인덱스 l부터 r까지)의 원소 합을 구합니다.

  2. 업데이트: 특정 인덱스 i의 원소 값을 변경합니다.

일반적인 접근법과 그 한계를 살펴보겠습니다.

바로 이 지점에서 세그먼트 트리가 등장합니다. 세그먼트 트리는 균형 잡힌 해결책을 제공합니다.


세그먼트 트리의 구조 🌳

세그먼트 트리는 배열 위에 구축되는 완전 이진 트리(full binary tree) 입니다.

예시:

배열 A = [2, 5, 1, 4, 9, 3]을 생각해 봅시다. 구간 합에 대한 세그먼트 트리는 다음과 같은 형태가 됩니다.


생성 방법

세그먼트 트리는 보통 이진 힙(binary heap)처럼 배열을 사용하여 구현됩니다. 만약 어떤 노드가 인덱스 i에 있다면, 그 왼쪽 자식은 2*i + 1, 오른쪽 자식은 2*i + 2에 위치합니다. 트리를 저장하는 데 필요한 배열의 크기는 안전하게 4n 정도로 잡습니다.

트리는 재귀적으로 생성됩니다.

build(node_index, start, end) 함수:

  1. 기저 조건 (Base Case): start == end이면, 리프 노드에 도달한 것입니다. tree[node_index] = array[start]로 설정합니다.

  2. 재귀 단계 (Recursive Step):

    • 중간 지점을 계산합니다: mid = (start + end) / 2.

    • 왼쪽 자식에 대해 [start, mid] 구간을 처리하도록 build 함수를 재귀적으로 호출합니다.

    • 오른쪽 자식에 대해 [mid + 1, end] 구간을 처리하도록 build 함수를 재귀적으로 호출합니다.

    • 현재 노드의 값은 자식 노드들의 값을 조합하여 결정됩니다: tree[node_index] = tree[left_child] + tree[right_child].

초기 호출은 build(0, 0, n-1)입니다. 전체 생성 과정은 트리의 각 노드를 정확히 한 번씩 방문하므로 O(n) 의 시간이 걸립니다.


연산

1. 구간 쿼리: query(l, r)

세그먼트 트리의 강력함은 모든 원소를 방문하지 않고도 [l, r] 구간에 대한 쿼리에 답할 수 있다는 점입니다. 쿼리 함수는 현재 노드가 담당하는 구간 [start, end]와 쿼리 구간 [l, r]의 관계를 세 가지 경우로 나누어 재귀적으로 동작합니다.

query(node_index, start, end, l, r) 함수:

이 과정은 매우 효율적입니다. 트리의 각 레벨에서 쿼리 경로는 멈추거나(완전 포함) 계속 내려갑니다. 경로는 최대 한 번만 두 갈래로 나뉠 수 있습니다. 트리의 높이가 $O(\log n)$이므로, 쿼리 시간은 **O(logn)**입니다.

2. 점 업데이트: update(index, value)

원본 배열의 한 원소를 업데이트하려면, 세그먼트 트리에서 그에 해당하는 노드들도 업데이트해야 합니다.

update(node_index, start, end, idx, val) 함수:

  1. 기저 조건: start == end이면, 인덱스 idx에 해당하는 리프 노드를 찾은 것입니다. 그 값을 업데이트합니다: tree[node_index] = val.

  2. 재귀 단계:

    • mid 지점을 찾습니다.

    • idx가 왼쪽 절반에 있는지 오른쪽 절반에 있는지 판단하여 적절한 자식에게 재귀 호출을 합니다.

    • 재귀 호출이 반환된 후, (업데이트되었을 수 있는) 자식 노드들의 값을 기반으로 현재 노드의 값을 다시 계산합니다: tree[node_index] = tree[left_child] + tree[right_child].

이 업데이트 과정은 루트에서 리프까지 단일 경로를 따라 내려갔다가 다시 올라옵니다. 트리의 높이가 $O(\log n)$이므로, 업데이트 시간 또한 **O(logn)**입니다.


심화 주제: Lazy Propagation

표준 세그먼트 트리는 점 업데이트에는 효율적입니다. 하지만 "인덱스 l부터 r까지 모든 원소에 5를 더하라"와 같은 구간 업데이트를 수행해야 한다면 어떨까요? 각 점을 개별적으로 업데이트하는 것은 너무 느릴 것입니다($O(nlogn)$).

Lazy Propagation은 이 문제를 해결하는 기법입니다. 아이디어는 업데이트를 지연시키는 것입니다. 구간 업데이트가 어떤 노드의 전체 구간을 포함할 때, 그 자식들을 모두 업데이트하는 대신, 해당 노드에 "게으른(lazy)" 태그를 저장하여 보류 중인 업데이트가 있음을 표시합니다. 그리고 꼭 필요할 때(즉, 후속 쿼리나 업데이트가 자식 노드에 접근해야 할 때)에만 이 업데이트를 아래로 "전파(propagate)"합니다.

Lazy Propagation을 사용하면 구간 업데이트 또한 O(logn) 시간에 처리할 수 있습니다.

결론

세그먼트 트리는 경쟁 프로그래밍과 알고리즘 설계에서 매우 중요한 자료구조입니다. 구간 쿼리와 업데이트를 모두 효율적으로 수행하는 능력 덕분에, 구간에 대한 동적 데이터를 다루는 문제에 적합합니다. 주된 용도는 합을 구하는 것이지만, 구간 최솟값/최댓값, 곱, 최대공약수(GCD) 등 결합 법칙이 성립하는 모든 연산에 적용할 수 있습니다.

references